/* Display.cpp
Copyright (c) 2008 Michael Zahniser
Please note the license terms (MIT license) at the end of this document.

Function definitions for the Display class.
*/

#include "Display.h"

#include "SDL.h"
#include "SDL_image.h"
#include "_sdl/EmbeddedFont.h"

#include "KingScraper.h"
#include "GoScraper.h"
#include "XKCDScraper.h"
#include "DilbertScraper.h"
#include "ComicsScraper.h"

#include <fstream>
#include <iostream>
#include <algorithm>

using std::ifstream;
using std::ofstream;
using std::cerr;
using std::endl;
using std::min;
using std::max;



// Constructor:
// Attempt to load all the resources.
Display::Display(const string &resourcePath)
	: resourcePath(resourcePath), 
	showPreferences(false), preferenceScroll(0), preferenceSelect(0)
{
	Initialize();
}



// Destructor:
Display::~Display()
{
	// Save the preferences. Currently this just saves the index of the most recently
	// viewed day for each comic.
	SavePreferences();
	
	// Free the Scrapers.
	while(!scrapers.empty())
	{
		delete scrapers.back();
		scrapers.pop_back();
	}
	if(isInitialized)
		SDL_Quit();
}



// Run:
// Run the event loop for the Display, returning when the user quits.
void Display::Run()
{
	// Expect the user to press the TAB key.
	previousKey = SDLK_TAB;
	comicChanged = true;
	
	while(true)
	{
		// Draw the current comic.
		Draw();
		
		// Preemptively load the next comic, _unless_ another key press has already
		// come in that we need to deal with.
		SDL_Event event;
		if(!SDL_PollEvent(&event))
		{
			// Don't preload if there's already another user event waiting.
			if(comicChanged)
			{
				next = scraper;
				nextScraper = DoKey(previousKey, next);
				comicChanged = false;
			}
			
			// Wait for the next user event.
			SDL_WaitEvent(&event);
		}
		
		if(event.type == SDL_QUIT)
			break;
		else if(event.type == SDL_KEYDOWN)
		{
			SDLKey key = event.key.keysym.sym;
			SDLMod mod = event.key.keysym.mod;
			
			// The user can type ESCAPE, Ctrl-Q, or Cmd-Q (on the Mac) to quit.
			if(key == SDLK_ESCAPE || (key == 'q' && (mod & (KMOD_META | KMOD_CTRL))))
				return;
			else if(key == '`' && !showPreferences)
				showPreferences = true;
			else if(showPreferences)
			{
				if(key == SDLK_UP && preferenceSelect)
					--preferenceSelect;
				else if(key == SDLK_DOWN && preferenceSelect < static_cast<int>(active.size() - 1))
					++preferenceSelect;
				else if(key == SDLK_SPACE)
					active[preferenceSelect] = !active[preferenceSelect];
				else if(key == '`')
				{
					// Only leave preferences mode if at least one comic is active.
					for(vector<bool>::const_iterator it = active.begin(); it != active.end(); ++it)
						if(*it)
						{
							showPreferences = false;
							break;
						}
				}
			}
			else if(key == SDLK_SPACE)
				scroll += SCROLL_AMOUNT;
			else if(key == previousKey && nextScraper)
			{
				// If the user presses the key that we're expected, and if we have already
				// loaded the image for that key, all we need to do is display it.
				scraper = next;
				delete *scraper;
				*scraper = nextScraper;
				nextScraper = NULL;
				
				LoadImage();
			}
			else if(Scraper *newScraper = DoKey(key, scraper))
			{
				// If the user presses an unexpected key, load the image for that key.
				// If the key is not recognized, do nothing.
				delete *scraper;
				*scraper = newScraper;
				nextScraper = NULL;
				
				previousKey = key;
				LoadImage();
			}
		}
	}
}



// Initialize:
// Load the resources, and start up libsdl and libcurl.
void Display::Initialize()
{
	if(SDL_Init(SDL_INIT_VIDEO) < 0)
	{
		cerr << "Unable to initialize SDL." << endl;
		return;
	}

	font.Load(EmbeddedFont::DATA, EmbeddedFont::WIDTH, EmbeddedFont::HEIGHT);
	
	// Set the resource path to the home directory, if we have one.
#ifndef __WIN32__
	resourcePath = getenv("HOME");
	resourcePath += '/';
#endif

	window.SetCaption("Mirth");
	SDL_SetEventFilter(&EventFilter);
	SDL_EnableKeyRepeat(SDL_DEFAULT_REPEAT_DELAY, SDL_DEFAULT_REPEAT_INTERVAL);
	
	// Initialize the internet connection.
	curl.Init();

	if(!curl)
		return;
	
	// Create the array of web scrapers.
	MakeScrapers();

	// Load the image for the first scraper.
	(**scraper).Reload(imageBuffer, curl);
	LoadImage();
	
	// Attempt to create the window. If successful, we are fully initialized.
	MakeWindow();
	if(window)
		isInitialized = true;
}



// MakeScrapers:
// Initialize the array of web scrapers.
void Display::MakeScrapers()
{
	scrapers.push_back(new GoScraper("ad", "Adam@Home by Brian Basset"));
	scrapers.push_back(new ComicsScraper("arlo&janis", "Arlo and Janis by Jimmy Johnson"));
	scrapers.push_back(new GoScraper("ch", "Calvin and Hobbes by Bill Watterson"));
	scrapers.push_back(new KingScraper("Curtis", "Curtis by Ray Billingsley"));
	scrapers.push_back(new DilbertScraper());
	scrapers.push_back(new GoScraper("db", "Doonesbury by Garry Trudeau"));
	scrapers.push_back(new KingScraper("Family_Circus", "Family Circus by Bil Keane"));
	scrapers.push_back(new GoScraper("fb", "For Better or For Worse by Lynn Johnston"));
	scrapers.push_back(new GoScraper("ft", "FoxTrot by Bill Amend"));
	scrapers.push_back(new GoScraper("ga", "Garfield by Jim Davis"));
	scrapers.push_back(new ComicsScraper("get_fuzzy", "Get Fuzzy by Darby Conley"));
	scrapers.push_back(new KingScraper("Mallard_Fillmore", "Mallard Fillmore by Bruce Tinsley"));
	scrapers.push_back(new KingScraper("Mgoose", "Mother Goose & Grimm by Mike Peters"));
	scrapers.push_back(new GoScraper("nq", "Non Sequitur by Wiley Miller"));
	scrapers.push_back(new ComicsScraper("rose_is_rose", "Rose is Rose by Pat Brady & Dom Winner"));
	scrapers.push_back(new KingScraper("Rhymes_with_Orange", "Rhymes With Orange by Hilary Price"));
	scrapers.push_back(new KingScraper("Shoe", "Shoe by Chris Cassatt, Gary Brookins & Susie MacNelly"));
	scrapers.push_back(new GoScraper("ss", "Stone Soup by Jan Eliot"));
	scrapers.push_back(new XKCDScraper());
	scrapers.push_back(new KingScraper("Zippy_the_Pinhead", "Zippy the Pinhead by Bill Griffith"));
	scrapers.push_back(new KingScraper("Zits", "Zits by Jerry Scott & Jim Borgman"));
	
	// Load the current index of each scraper from the preferences file.
	// If there is none, the indices are initialized to today's date.
	ifstream in((resourcePath + ".mirth").c_str());
	for(scraper = scrapers.begin(); scraper != scrapers.end(); ++scraper)
		(**scraper).Load(in);
	
	bool i;
	while(in >> i)
		active.push_back(i);
	active.resize(scrapers.size(), true);
	
	// Start at the top of the list.
	scraper = scrapers.begin();
}



// SavePreferences:
// Save the preferences (current date for each comic).
// Eventually this will also include whether or not to view each comic.
void Display::SavePreferences()
{
	ofstream out((resourcePath + ".mirth").c_str());
	
	for(scraper = scrapers.begin(); scraper != scrapers.end(); ++scraper)
		(**scraper).Save(out);
	
	out << endl;
	
	for(vector<bool>::const_iterator it = active.begin(); it != active.end(); ++it)
		out << *it << ' ';
	
	out << endl;
}



// LoadImage:
// Load an image from the buffer.
void Display::LoadImage()
{
	const string &type = (**scraper).ImageType();
	
	// Now, the image is in imageBuffer.
	// Get it into an SDL surface.
	SDL_RWops *imageFile = SDL_RWFromConstMem(imageBuffer.data(), static_cast<int>(imageBuffer.size()));
	if(type == "gif")
		image = IMG_LoadGIF_RW(imageFile);
	else if(type == "png")
		image = IMG_LoadPNG_RW(imageFile);
	else if(type == "jpg")
		image = IMG_LoadJPG_RW(imageFile);
	else
		return;
	
	comicChanged = true;
	SDL_FreeRW(imageFile);
	
	scroll = 0;
}



// MakeWindow:
// Create the window, big enough to include the current image, with the given
// number of additional pixels space on the bottom for the status bar.
void Display::MakeWindow(int statusHeight)
{
	// Expand the window if needed to fit this image.
	// But don't expand it to more than the maximum height.
	int newHeight = MAXIMUM_WINDOW_HEIGHT;
	newHeight = min(newHeight,
		max(image.Height() + 16 + statusHeight, window.Height()));

	// Expand the width to fit the new comic.
	int newWidth = max(image.Width() + 16, window.Width());
	
	// If the window size has changed, allocate a new window.
	window.Resize(newWidth, newHeight);
}



// Draw:
// Draw the window.
void Display::Draw()
{
	if(showPreferences)
	{
		DrawPreferences();
		return;
	}
	// Get the description text. We need to figure out how many lines it will occupy.
	string description = (**scraper).Description();

	// Estimate how many rows of text the description should be split into.
	int windowWidth = max(window.Width(), image.Width() + 16);
	int rows = 1 + font.Width(description) / (windowWidth - 64);
	int statusHeight = 16 * rows;
	// Resize the window, if necessary.
	MakeWindow(statusHeight);
	
	// Erase the window.
	window.Erase(window.Color(255));
	
	// If the image fits in the window, draw it centered. Otherwise, draw it scrolled
	// by the current scroll amount.
	if(image.Height() <= window.Height() - statusHeight)
		image.Blit(window, (window.Width() - image.Width()) / 2, (window.Height() - image.Height() - statusHeight) / 2);
	else
		image.Blit(window, (window.Width() - image.Width()) / 2, -scroll);
	
	// Fill in the status bar region.
	int barY = window.Height() - statusHeight;
	window.FillRect(0, barY, window.Width(), statusHeight, window.Color(200));
	window.HLine(0, barY, window.Width(), window.Color(0));
	
	// Draw the description text, splitting it into multiple lines as necessary.
	// Note: this is a quick and dirty way of doing word wrapping, but it usually seems to work.
	// It may be confused by very long words.
	string::const_iterator it = description.begin();
	for(int i = 0; i < rows; ++i)
	{
		string::const_iterator end = description.begin() + ((i + 1) * description.length()) / rows;
		while(end < description.end() && *end > ' ')
			++end;
		
		font.Draw(it, end, window, (window.Width() - font.Width(it, end)) / 2, barY + 1);
		barY += 16;
		it = end;
	}
	
	// Update the window.
	window.Update();
}



void Display::DrawPreferences()
{
	// Erase the window.
	window.Erase(window.Color(255));
	
	// Figure out how many lines fit in the window.
	static const int LINE = 20;
	int rows = window.Height() / LINE;
	
	// Make sure the selected line is visible.
	if(preferenceSelect < preferenceScroll)
		preferenceScroll -= rows - 2;
	if(preferenceSelect >= preferenceScroll + rows)
		preferenceScroll += rows - 2;
	preferenceScroll = min(static_cast<int>(active.size() - rows), max(0, preferenceScroll));
	
	for(int row = 0; row < rows; ++row)
	{
		int index = row + preferenceScroll;
		if(index == preferenceSelect)
		{
			Uint32 color = active[index] ? window.Color(224, 240, 248) : window.Color(192, 224, 240);
			window.FillRect(0, row * LINE, window.Width(), LINE, color);
		}
		
		string text = scrapers[index]->Description();
		font.Draw(text, window, 4, row * LINE + (LINE - font.Height()) / 2);
		
		if(!active[index])
			window.OverlayRect(0, row * LINE, window.Width(), LINE, window.Color(255));
	}
	
	// Update the window.
	window.Update();
}



// DoKey:
// Handle the given key press, returning a new Scraper object that points to the
// image that the given key navigates to. Return NULL if the key is not recognized
// or if the image does not exist (for example, if the image is for a future date).
Scraper *Display::DoKey(SDLKey key, vector<Scraper *>::iterator &it)
{
	// Keep toggling between scrapers until we find an active one.
	do {
		// Up and down move through the list of scrapers. TAB is equivalent to DOWN
		// plus RIGHT.
		if(key == SDLK_UP)
		{
			if(it == scrapers.begin())
				it = scrapers.end();
			--it;
		}
		if(key == SDLK_DOWN || key == SDLK_TAB)
		{
			++it;
			if(it == scrapers.end())
				it = scrapers.begin();
		}
	} while(!active[it - scrapers.begin()]);
	
	// Make a clone of the scraper. We will move this clone forward or backward
	// based on the key.
	Scraper *scraper = (**it).Clone();
	
	if(key == SDLK_LEFT)
	{
		if(scraper->Previous(imageBuffer, curl))
			return scraper;
	}
	if(key == SDLK_RIGHT || key == SDLK_TAB)
	{
		if(scraper->Next(imageBuffer, curl))
			return scraper;
	}
	if(key == SDLK_TAB || key == SDLK_DOWN || key == SDLK_UP)
	{
		// If the user pressed the TAB key but there is no next image for the next comic,
		// try loading the image for the current date instead.
		if(scraper->Reload(imageBuffer, curl))
			return scraper;
	}
	
	// The key was not recognized, or the image could not be loaded.
	return NULL;
}



// EventFilter:
// Filter that rejects all events not used by this application.
int Display::EventFilter(const SDL_Event *event)
{
	return (event->type == SDL_QUIT) | (event->type == SDL_KEYDOWN);
}



/* Copyright (c) 2008 Michael Zahniser

Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE. */
